package eva

import (
	"testing"

	"gitcode.com/deyiyangyang/gascheme/ast"
	"gitcode.com/deyiyangyang/gascheme/obj"
	"gitcode.com/deyiyangyang/gascheme/tok"
)

func TestSetCar(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Create a pair (cons 1 2)
	pair := &obj.List{
		Car: &obj.Integer{Value: 1},
		Cdr: &obj.Integer{Value: 2},
	}

	// First, store the pair in the environment
	env.Set("pair", pair)

	// Evaluate the set-car! expression
	// We need to manually call the function since we're testing the builtin directly
	result := SetCar([]obj.Object{pair, &obj.Integer{Value: 42}}, env)

	// Check that set-car! returned void
	if result.Type() != obj.VOID_OBJ {
		t.Errorf("set-car! should return void, got %s", result.Type())
	}

	// Check that the car of the pair was modified
	if pair.Car.(*obj.Integer).Value != 42 {
		t.Errorf("set-car! did not modify the car. Got %d, want 42", pair.Car.(*obj.Integer).Value)
	}

	// Check that the cdr is unchanged
	if pair.Cdr.(*obj.Integer).Value != 2 {
		t.Errorf("set-car! modified the cdr. Got %d, want 2", pair.Cdr.(*obj.Integer).Value)
	}
}

func TestSetCdr(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Create a pair (cons 1 2)
	pair := &obj.List{
		Car: &obj.Integer{Value: 1},
		Cdr: &obj.Integer{Value: 2},
	}

	// First, store the pair in the environment
	env.Set("pair", pair)

	// Evaluate the set-cdr! expression
	// We need to manually call the function since we're testing the builtin directly
	result := SetCdr([]obj.Object{pair, &obj.Integer{Value: 42}}, env)

	// Check that set-cdr! returned void
	if result.Type() != obj.VOID_OBJ {
		t.Errorf("set-cdr! should return void, got %s", result.Type())
	}

	// Check that the cdr of the pair was modified
	if pair.Cdr.(*obj.Integer).Value != 42 {
		t.Errorf("set-cdr! did not modify the cdr. Got %d, want 42", pair.Cdr.(*obj.Integer).Value)
	}

	// Check that the car is unchanged
	if pair.Car.(*obj.Integer).Value != 1 {
		t.Errorf("set-cdr! modified the car. Got %d, want 1", pair.Car.(*obj.Integer).Value)
	}
}

func TestSetCarWithList(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Create a proper list '(2 3)
	properList := &obj.List{
		Car: &obj.Integer{Value: 2},
		Cdr: &obj.List{
			Car: &obj.Integer{Value: 3},
			Cdr: &obj.List{},
		},
	}

	// Create a pair (cons 1 '(2 3))
	pair := &obj.List{
		Car: &obj.Integer{Value: 1},
		Cdr: properList,
	}

	// Store the pair in the environment
	env.Set("pair", pair)

	// Apply set-car! to change the car to 'new-value
	newValue := &obj.Symbol{Value: "new-value"}
	SetCar([]obj.Object{pair, newValue}, env)

	// Check that the car was modified
	sym, ok := pair.Car.(*obj.Symbol)
	if !ok {
		t.Errorf("Car should be a symbol, got %T", pair.Car)
		return
	}

	if sym.Value != "new-value" {
		t.Errorf("Car should be 'new-value, got '%s'", sym.Value)
	}

	// Check that the cdr is unchanged
	if pair.Cdr != properList {
		t.Errorf("Cdr should be unchanged")
	}
}

func TestSetCdrWithList(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Create a proper list '(2 3)
	properList := &obj.List{
		Car: &obj.Integer{Value: 2},
		Cdr: &obj.List{
			Car: &obj.Integer{Value: 3},
			Cdr: &obj.List{},
		},
	}

	// Create a pair (cons 1 '(2 3))
	pair := &obj.List{
		Car: &obj.Integer{Value: 1},
		Cdr: properList,
	}

	// Store the pair in the environment
	env.Set("pair", pair)

	// Apply set-cdr! to change the cdr to 'new-value
	newValue := &obj.Symbol{Value: "new-value"}
	SetCdr([]obj.Object{pair, newValue}, env)

	// Check that the cdr was modified
	sym, ok := pair.Cdr.(*obj.Symbol)
	if !ok {
		t.Errorf("Cdr should be a symbol, got %T", pair.Cdr)
		return
	}

	if sym.Value != "new-value" {
		t.Errorf("Cdr should be 'new-value, got '%s'", sym.Value)
	}

	// Check that the car is unchanged
	if pair.Car.(*obj.Integer).Value != 1 {
		t.Errorf("Car should be unchanged")
	}
}

func TestSetCarErrorCases(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Test with wrong number of arguments
	result1 := SetCar([]obj.Object{}, env)
	if result1.Type() != obj.ERROR_OBJ {
		t.Errorf("set-car! with 0 arguments should return an error, got %s", result1.Type())
	}

	result2 := SetCar([]obj.Object{&obj.Integer{Value: 1}}, env)
	if result2.Type() != obj.ERROR_OBJ {
		t.Errorf("set-car! with 1 argument should return an error, got %s", result2.Type())
	}

	result3 := SetCar([]obj.Object{
		&obj.Integer{Value: 1},
		&obj.Integer{Value: 2},
		&obj.Integer{Value: 3},
	}, env)
	if result3.Type() != obj.ERROR_OBJ {
		t.Errorf("set-car! with 3 arguments should return an error, got %s", result3.Type())
	}

	// Test with non-list first argument
	result4 := SetCar([]obj.Object{
		&obj.Integer{Value: 1},
		&obj.Integer{Value: 2},
	}, env)
	if result4.Type() != obj.ERROR_OBJ {
		t.Errorf("set-car! with non-list first argument should return an error, got %s", result4.Type())
	}
}

func TestSetCdrErrorCases(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Test with wrong number of arguments
	result1 := SetCdr([]obj.Object{}, env)
	if result1.Type() != obj.ERROR_OBJ {
		t.Errorf("set-cdr! with 0 arguments should return an error, got %s", result1.Type())
	}

	result2 := SetCdr([]obj.Object{&obj.Integer{Value: 1}}, env)
	if result2.Type() != obj.ERROR_OBJ {
		t.Errorf("set-cdr! with 1 argument should return an error, got %s", result2.Type())
	}

	result3 := SetCdr([]obj.Object{
		&obj.Integer{Value: 1},
		&obj.Integer{Value: 2},
		&obj.Integer{Value: 3},
	}, env)
	if result3.Type() != obj.ERROR_OBJ {
		t.Errorf("set-cdr! with 3 arguments should return an error, got %s", result3.Type())
	}

	// Test with non-list first argument
	result4 := SetCdr([]obj.Object{
		&obj.Integer{Value: 1},
		&obj.Integer{Value: 2},
	}, env)
	if result4.Type() != obj.ERROR_OBJ {
		t.Errorf("set-cdr! with non-list first argument should return an error, got %s", result4.Type())
	}
}

func TestSetCarCdrWithEvaluator(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Create test expression: (begin 
	//                         (define p (cons 1 2)) 
	//                         (set-car! p 42) 
	//                         (car p))
	beginExpr := &ast.ListExpression{
		Token: tok.Token{Type: tok.LPAREN, Lexeme: "("},
		Elements: []ast.Expression{
			&ast.SymbolLiteral{Value: "begin"},
			&ast.ListExpression{
				Elements: []ast.Expression{
					&ast.SymbolLiteral{Value: "define"},
					&ast.SymbolLiteral{Value: "p"},
					&ast.ListExpression{
						Elements: []ast.Expression{
							&ast.SymbolLiteral{Value: "cons"},
							&ast.IntegerLiteral{Value: 1},
							&ast.IntegerLiteral{Value: 2},
						},
					},
				},
			},
			&ast.ListExpression{
				Elements: []ast.Expression{
					&ast.SymbolLiteral{Value: "set-car!"},
					&ast.SymbolLiteral{Value: "p"},
					&ast.IntegerLiteral{Value: 42},
				},
			},
			&ast.ListExpression{
				Elements: []ast.Expression{
					&ast.SymbolLiteral{Value: "car"},
					&ast.SymbolLiteral{Value: "p"},
				},
			},
		},
	}

	// Evaluate the expression
	result := Eval(beginExpr, env)

	// Check the result
	intResult, ok := result.(*obj.Integer)
	if !ok {
		t.Errorf("Result should be an integer, got %T", result)
		return
	}

	if intResult.Value != 42 {
		t.Errorf("Result should be 42, got %d", intResult.Value)
	}
}

func TestSetCdrWithEvaluator(t *testing.T) {
	// Create an environment and register built-in procedures
	env := obj.NewGlobalEnvironment()
	RegisterBuiltinProcedures(env)

	// Create test expression: (begin 
	//                         (define p (cons 1 2)) 
	//                         (set-cdr! p 42) 
	//                         (cdr p))
	beginExpr := &ast.ListExpression{
		Token: tok.Token{Type: tok.LPAREN, Lexeme: "("},
		Elements: []ast.Expression{
			&ast.SymbolLiteral{Value: "begin"},
			&ast.ListExpression{
				Elements: []ast.Expression{
					&ast.SymbolLiteral{Value: "define"},
					&ast.SymbolLiteral{Value: "p"},
					&ast.ListExpression{
						Elements: []ast.Expression{
							&ast.SymbolLiteral{Value: "cons"},
							&ast.IntegerLiteral{Value: 1},
							&ast.IntegerLiteral{Value: 2},
						},
					},
				},
			},
			&ast.ListExpression{
				Elements: []ast.Expression{
					&ast.SymbolLiteral{Value: "set-cdr!"},
					&ast.SymbolLiteral{Value: "p"},
					&ast.IntegerLiteral{Value: 42},
				},
			},
			&ast.ListExpression{
				Elements: []ast.Expression{
					&ast.SymbolLiteral{Value: "cdr"},
					&ast.SymbolLiteral{Value: "p"},
				},
			},
		},
	}

	// Evaluate the expression
	result := Eval(beginExpr, env)

	// Check the result
	intResult, ok := result.(*obj.Integer)
	if !ok {
		t.Errorf("Result should be an integer, got %T", result)
		return
	}

	if intResult.Value != 42 {
		t.Errorf("Result should be 42, got %d", intResult.Value)
	}
}